import * as yaml from 'js-yaml';
import * as nacos from 'nacos';
import Logger from './logger';

const SHARED_CONFIG_DEFAULT_GROUP = 'DEFAULT_GROUP';

export class SharedConfig {
  dataId!: string;
  group?: string;
  refresh?: boolean;
}

type Config = { [key: string]: any };

export class ConfigManager {
  private definitions: Map<string, SharedConfig> = new Map<string, SharedConfig>();
  private rawContentCache: Map<string, string> = new Map<string, string>();
  private propertiesCache: Config = {};
  private loadedCache: Map<string, boolean> = new Map<string, boolean>();
  private subscribeCache: Map<string, boolean> = new Map<string, boolean>();
  private logger = new Logger('ConfigManagerLogger');
  private debug = false;

  constructor(private readonly client: nacos.NacosConfigClient) {
  }

  public enableDebug(): void {
    this.debug = true;
  }

  public disableDebug(): void {
    this.debug = false;
  }

  public addConfigs(configs: SharedConfig[]): void {
    if (!configs || configs.length <= 0) {
      return;
    }

    configs.forEach((config) => this.addConfig(config));
    this.loadConfig();
  }

  private loadFinished(): boolean {
    for (const finished of this.loadedCache.values()) {
      if (!finished) {
        return false;
      }
    }

    return true;
  }

  public async getAllConfig(): Promise<Config> {
    return new Promise((resolve) => {
      const intervalId = setInterval(() => {
        if (this.loadFinished()) {
          clearInterval(intervalId);
          resolve(JSON.parse(JSON.stringify(this.propertiesCache)));
        }
      }, 100);
    });
  }


  public getSharedConfig(): { [key: string]: SharedConfig } {
    const result: { [key: string]: SharedConfig } = {};
    for (const key of this.definitions.keys()) {
      result[key] = {...this.definitions.get(key)};
    }

    return result;
  }

  public async getConfig(key: string, defaultValue?: string): Promise<string | null> {
    key = formatKey(key);
    const envConfig = loadConfigByLocalEnv(key);

    if (envConfig !== null) {
      return envConfig;
    }

    return new Promise((resolve) => {
      const intervalId = setInterval(() => {
        if (this.loadFinished()) {
          clearInterval(intervalId);
          const value = this.loadConfigValue(key, this.propertiesCache);

          if (value === null) {
            if (defaultValue !== undefined) {
              resolve(defaultValue);
            } else {
              resolve(null);
            }
          } else {
            resolve(value);
          }
        }
      }, 100);
    });
  }

  private loadConfigValue(key: string, configCache: Config): string | null {
    if (configCache === undefined || configCache === null) {
      return null;
    }

    const keys = key.split('.');
    const currentKey = keys[0];
    const arrayReg = new RegExp('\\[\\d\\]', 'gm');
    const isArray = arrayReg.test(currentKey);
    let currentValue;

    if (isArray) {
      const regExpMatchArray: RegExpMatchArray = currentKey.match(arrayReg);
      const currentKeyPrefix = currentKey.substr(0, currentKey.indexOf('['));

      currentValue = configCache[currentKeyPrefix];

      if (currentValue === null || currentValue === undefined) {
        return null;
      }

      for (const index of regExpMatchArray) {
        const indexKey = index.substr(1, index.length - 2);
        currentValue = currentValue[indexKey];

        if (currentValue === null || currentValue === undefined) {
          return null;
        }
      }
    } else {
      currentValue = configCache[currentKey];
    }

    if (keys.length > 1) {
      return this.loadConfigValue(keys.slice(1).join('.'), currentValue);
    } else {
      const value = currentValue;
      return value === undefined || value === null ? null : value;
    }
  }

  private addConfig(config: SharedConfig): void {
    if (config.group === undefined) {
      config.group = SHARED_CONFIG_DEFAULT_GROUP;
    }
    if (config.refresh === undefined) {
      config.refresh = false;
    }
    const key = ConfigManager.buildConfigKey(config);
    if (this.debug) {
      this.logger.debug('add config', key, config);
    }
    this.definitions.set(key, config);
    this.rawContentCache.set(key, '');
    this.loadedCache.set(key, false);
    this.subscribeCache.set(key, false);
  }

  private static buildConfigKey(config: SharedConfig): string {
    return config.dataId + '@' + config.group;
  }

  private loadConfig(): void {
    for (const key of this.definitions.keys()) {
      this.loadConfig0(key, this.definitions.get(key));
    }
  }

  private loadConfig0(key: string, {dataId, group, refresh}: SharedConfig): void {
    this.client.getConfig(dataId, group).then((rawContent) => {
      this.parseConfig(key, rawContent);
    });
    if (refresh && !this.subscribeCache.get(key)) {
      this.client.subscribe({
        dataId,
        group
      }, (rawContent) => {
        if (this.debug) {
          this.logger.debug('invoke config subscribe', key, rawContent);
        }
        this.parseConfig(key, rawContent);
      });
      if (this.debug) {
        this.logger.debug('add config subscribe', key);
      }
      this.subscribeCache.set(key, true);
    }
  }

  private parseConfig(key: string, rawContent: string): void {
    this.loadedCache.set(key, true);
    this.rawContentCache.set(key, rawContent);
    const config = yaml.safeLoad(rawContent);
    if (typeof config === 'object') {
      this.propertiesCache = {...this.propertiesCache, ...this.formatConfig(config)};
    }
  }

  private formatConfig(config: Config): Config {
    const cache: Config = {};
    if (config === null || config === undefined) {
      return cache;
    }
    for (let key of Object.keys(config)) {
      const value = config[key];
      key = formatKey(key);
      if (typeof value === 'object') {
        cache[key] = this.formatConfig(value);
      } else {
        cache[key] = value;
      }
    }

    return cache;
  }
}

export function loadConfigByLocalEnv(key: string, defaultValue?: string): string | null {
  key = dotToLine(key).toUpperCase();

  const value = process.env[key];

  if (typeof value === 'string') {
    return value;
  }

  return defaultValue !== null && defaultValue !== undefined ? defaultValue : null;
}

function formatKey(str: string): string {
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}

function dotToLine(str: string): string {
  return replaceAll(str, '\\.', '_');
}

function replaceAll(str: string, origin: string, target: string): string {
  return str.replace(new RegExp(origin, 'gm'), target);
}
